import { UITransform, view } from 'cc';
// Learn TypeScript:
import { Camera, Node, Tween, Vec2, Vec3, easing, tween, v2, v3 } from "cc";

export interface IGrid {
    crd: Vec2;
    init(crd: Vec2, root: Node, pos: Vec2): void;
    layer: number;
}
export default class GD {

    static create<T extends IGrid>(newGrid: (crd: Vec2) => T, root: Node, layers: Node[], camera: Camera, col: number, row: number, width: number, height: number) {
        let grid = new GridMap<T>();
        grid.init(newGrid, root, layers, camera, col, row, width, height);
        return grid;
    }
}
export class GridMap<T extends IGrid> {
    private get viewSize() {
        return view.getVisibleSize();
    }
    private grids: T[][] = [];
    col: number = 5;
    row: number = 5;
    width: number = 40;
    height: number = 40;
    root: Node;
    mapCamera: Camera;
    layers: Node[] = [];
    private get zero() { return v2(-this.width * this.col / 2, -this.height * this.row / 2); }

    init(newGrid: (crd: Vec2) => T, root: Node, layers: Node[], camera: Camera, col: number, row: number, width: number, height: number) {
        this.mapCamera = camera;
        this.root = root;
        this.layers = layers;
        this.col = col;
        this.row = row;
        this.width = width;
        this.height = height;
        for (let i = 0; i < this.col; i++) {
            this.grids.push([]);
            for (let j = 0; j < this.row; j++) {
                let grid = newGrid(v2(i, j));
                this.grids[i].push(grid);
                let pos = v2(this.width * i + this.width / 2, this.height * j + this.height / 2).add(this.zero);
                grid.init(v2(i, j), layers[grid.layer], pos);
            }
        }
    }
    findGrid(crd: Vec2) {
        if (!this.validCrd(crd))
            return null;
        return this.grids[crd.x][crd.y];
    }
    filterGrid(filter: (grid: T) => boolean) {
        let grids = [];
        this.grids.forEach(e => e.forEach(f => {
            if (filter(f))
                grids.push(f);
        }));
        return grids;
    }
    wp2lp(wp: Vec2) {
        return this.root.getComponent(UITransform).convertToNodeSpaceAR(v3(wp.x, wp.y, 0));

    }
    wp2crd(wp: Vec2) {
        let lp = this.wp2lp(wp);
        return this.lp2crd(v2(lp.x, lp.y));
    }
    lp2crd(lp: Vec2) {
        let norLp = lp.subtract(this.zero);
        let x = Math.floor(norLp.x / this.width);
        let y = Math.floor(norLp.y / this.height);
        return v2(x, y);
    }
    crd2lp(crd: Vec2) {
        return this.zero.add(v2(crd.x * this.width, crd.y * this.height)).add(v2(this.width / 2, this.height / 2))
    }
    isInScreen(crd: Vec2, top: number = 0, bottom: number = 0, left: number = 0, right: number = 0): Vec2 {
        let ldCrd = this.sp2crd(v2(left, bottom));
        let ruCrd = this.sp2crd(v2(this.viewSize.width - right, this.viewSize.height - top));
        let res = v2();
        if (crd.x < ldCrd.x)
            res.x = -1;
        else if (crd.x > ruCrd.x)
            res.x = 1;
        else if (crd.y > ldCrd.y)
            res.y = -1;
        else if (crd.y < ruCrd.y)
            res.y = 1;
        return res;
    }
    public validCrd2(x: number, y: number) {
        return x >= 0 && x < this.col && y >= 0 && y < this.row;
    }
    public validCrd(crd: Vec2) {
        return crd.x >= 0 && crd.x < this.col && crd.y >= 0 && crd.y < this.row;
    }
    public neighborGrids(center: Vec2, dist: number = 1) {

        let crd = center;
        let res: Vec2[] = [];
        for (let x = - dist; x <= dist; x++) {
            for (let y = -dist; y <= dist; y++) {
                let offset = v2(x, y);
                if (Math.abs(offset.x) + Math.abs(offset.y) > dist || offset.equals(v2(0, 0)))
                    continue;
                let nextCrd = crd.add(v2(x, y));
                if (this.validCrd(nextCrd))
                    res.push(nextCrd);

            }
        }
        return res;
    }
    limitCameraPos(pos: Vec3) {
        // let res = v3(pos.x, pos.y);
        // let scale = this.getScaleByMap(this.map.colSize, row);
        // let maxY = Math.max(0, (this.isChallenge ? challengeHei : commonHei) * scale / 2 - this.viewSize.height / 2);
        // let minY = Math.min(0, -maxY);
        // let maxX = Math.max(0, levelWidth * scale / 2 - this.viewSize.width / 2);
        // let minX = Math.min(0, -maxX);

        // res.x = misc.clampf(res.x, minX, maxX);
        // res.y = misc.clampf(res.y, minY, maxY);
        return pos;

    }
    moveDelta(delta: Vec2) {
        let originPos = this.mapCamera.node.position;
        let pos = originPos.subtract(v3(delta.x, delta.y));
        let newPos = this.limitCameraPos(pos);
        this.mapCamera.node.position = v3(newPos);
        return newPos.x != originPos.x || newPos.y != originPos.y;
    }
    cameraTween: Tween<Node> = null;
    moveFocusTo(crd: Vec2, updateCall?: Function) {
        let lp = this.crd2lp(crd);
        let wp = this.root.getComponent(UITransform).convertToWorldSpaceAR(v3(lp.x, lp.y, 0));
        let nowCenter = this.mapCamera.screenToWorld(v3(this.viewSize.width / 2, this.viewSize.height / 2, 0))
        let delta = v3(wp).subtract(nowCenter);
        let targetPos = this.mapCamera.node.position.add(delta);
        targetPos = this.limitCameraPos(targetPos);
        this.cameraTween?.stop();
        this.cameraTween = tween(this.mapCamera.node).to(0.5, { position: targetPos }, { easing: easing.sineOut, onUpdate: () => updateCall && updateCall() }).call(() => this.cameraTween = null).start();
    }
    /**
     * Find all the grid starting from a given coordinate.
     *
     * @param {Vec2} startCrd - 起点
     * @param {(crd: Vec2) => boolean} validSearchCrd - 搜索条件（如果为false，则不会添加该格子到结果中）
     * @param {(crd: Vec2) => boolean} validGridCrd - 合法条件（如果搜索条件和合法条件同时为true，才会添加到格子中）
     * @return {Vec2[]} array of valid grid coordinates
     */
    public findAllGrid(startCrd: Vec2, validSearchCrd: (crd: Vec2) => boolean, validGridCrd: (crd: Vec2) => boolean = crd => true) {
        let res: Vec2[] = [];
        let stack: Vec2[] = [];
        let curCrd = startCrd;
        let visit: Vec2[] = [];
        stack.unshift(startCrd);
        while (stack.length > 0) {
            curCrd = stack.shift();
            if (validGridCrd(curCrd))
                res.push(curCrd);
            visit.push(curCrd);
            let nears = this.neighborGrids(curCrd);
            for (let i = 0; i < nears.length; i++) {
                let crd = nears[i];
                if (visit.findIndex(e => e.equals(crd)) < 0 && this.validCrd(crd) && validSearchCrd(crd)) {
                    stack.unshift(crd);
                }
            }
        }
        return res;

    }
    public findPath(startCrd: Vec2, endCrd: Vec2, validGridCrd: (crd: Vec2) => boolean) {
        let manhaton = (a: Vec2, b: Vec2) => {
            let len1 = Math.abs(a.x - b.x);
            let len2 = Math.abs(a.y - b.y);
            let min = Math.min(len1, len2);
            let max = Math.max(len1, len2);
            return Math.sqrt(2) * min + (max - min);
        }
        let visits: { crd: Vec2, cost: number, dist: number }[] = [];
        let stack: { crd: Vec2, cost: number, dist: number }[] = [];
        let curPath: Vec2[] = [];
        let curCrd = startCrd;
        let curCost = 0;
        let unshift = (crd: Vec2) => {
            let dist = manhaton(crd, endCrd);
            let newStep = Vec2.distance(curCrd, crd);
            if (newStep > 1) newStep *= 0.999;
            let cost = curCost + newStep;
            let visitInfo = visits.find(e => e.crd.equals(crd));
            let visitStack = stack.find(e => e.crd.equals(crd));
            if (visitStack && visitStack.cost + visitStack.dist > dist + cost) {

                visitStack.cost = cost;
                visitStack.dist = dist;
                visitStack.crd = crd;
                pathMap.set(crd, [...curPath, crd]);
            }
            else if (!visitStack && (!visitInfo || visitInfo.cost + visitInfo.dist > dist + cost)) {
                if (visitInfo) {
                }
                stack.unshift({ crd, cost, dist });
                pathMap.set(crd, [...curPath, crd]);
            }
        }
        let shift = () => {
            return stack.shift();
        }
        let pathMap: Map<Vec2, Vec2[]> = new Map();
        unshift(startCrd);
        while (stack.length > 0) {
            let curInfo = shift();
            curCrd = curInfo.crd;
            curCost = curInfo.cost;
            curPath = [...pathMap.get(curCrd)];

            visits.push(curInfo);
            if (curCrd.equals(endCrd)) {
                let crdList = pathMap.get(curCrd);
                return new PathInfo(crdList);
            }
            let nears = this.neighborGrids(curCrd);

            for (let i = 0; i < nears.length; i++) {
                let crd = nears[i];
                if (this.validCrd(crd) && validGridCrd(crd)) {
                    unshift(crd);
                }
            }
            stack.sort((a, b) => (a.dist + a.cost) - (b.dist + b.cost));
        }
        return new PathInfo([]);

    }
    private crd2lp2(x: number, y: number) {
        let hei = this.row * this.height;
        let wid = this.col * this.width;
        return v2(x * this.width - wid / 2 + this.width / 2, hei - y * this.height - hei / 2 - this.height / 2);
    }
    sp2crd(sp: Vec2) {
        let wp = this.mapCamera.screenToWorld(v3(sp.x, sp.y, 0));
        let lp = this.root.getComponent(UITransform).convertToNodeSpaceAR(wp);
        let zero = this.crd2lp2(0, 0).subtract(v2(this.width / 2, -this.height / 2));
        let x = Math.floor((lp.x - zero.x) / this.width);
        let y = Math.floor((lp.y - zero.y) / -this.height);
        return v2(x, y);
    }
}
export class PathInfo {
    public get isEmpty() { return this.pathCrds.length == 0; }
    public pathCrds: Vec2[] = [];
    public get startCrd() { return this.isEmpty ? null : this.pathCrds[0]; }
    public get endCrd() { return this.isEmpty ? null : this.pathCrds[this.pathCrds.length - 1]; }
    constructor(pathCrd: Vec2[] = []) {
        this.pathCrds = pathCrd;
    }
    public toString() {
        return this.pathCrds.map((v) => `(${v.x},${v.y})`).join("->");
    }
}
